19 设计模式——适配器模式

hello

返回设计模式博客目录

介绍


适配器(Adapter)模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

适配器模式在我们开发中使用率极高,从代码中随处可见的 Adapter 可以判断出来。从最早的 ListView、GridView 到现在最新的 RecyclerView 都需要使用 Adapter。说到底,适配器是将两个不兼容的类融合在一起,它有点像粘合剂,将不同的东西通过一种转换使得它们能够协作起来。

优点

  • 更好的复用性。系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。
  • 更好的扩展性。在实现适配器功能的时候,可以调用自己开发的工功能,从而自然地扩展系统的功能。

缺点

  • 过多地使用适配器,会让系统非常零乱,不易整体把握。

使用场景

  • 当想使用一个已经存在的类,但它的接口不符合需求时。
  • 当想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作。
  • 需要一个统一的输出接口,而输入端的类型不可预知。

结构与实现


类适配器模式可采用多重继承方式实现,如 C++ 可定义一个适配器类来同时继承当前系统的业务接口和现有组件库中已经存在的组件接口;Java 不支持多继承,但可以定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。

对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。现在来介绍它们的基本结构。

模式包含以下主要角色。

  • Target:目标接口,当前系统业务所期待的接口,它可以是抽象类或接口。
  • Adaptee:适配者类,它是被访问和适配的现存组件库中的组件接口。
  • Adapter:适配器类,它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

其结构图如下图所示。

类适配器模式的结构图

对象适配器模式的结构图

类适配器模式的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 目标接口
interface Target {
void request();
}
// 适配者接口
class Adaptee {
public void specificRequest() {
System.out.println("适配者中的业务代码被调用!");
}
}
// 类适配器类
class ClassAdapter extends Adaptee implements Target {
public void request() {
specificRequest();
}
}
// 客户端代码
public class ClassAdapterTest {
public static void main(String[] args) {
System.out.println("类适配器模式测试:");
Target target = new ClassAdapter();
target.request();
}
}

对象适配器模式的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 对象适配器类
class ObjectAdapter implements Target {
private Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void request() {
adaptee.specificRequest();
}
}
// 客户端代码
public class ObjectAdapterTest {
public static void main(String[] args) {
System.out.println("对象适配器模式测试:");
Adaptee adaptee = new Adaptee();
Target target = new ObjectAdapter(adaptee);
target.request();
}
}

说明:对象适配器模式中的“目标接口”和“适配者类”的代码同类适配器模式一样,只要修改适配器类和客户端的代码即可。与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到 Adaptee 类,而是使用代理关系

示例


用电源接口做例子,笔记本电脑的电源一般在 5V 电压,但是在我们生活中的电线电压一般是 220V。这个时候出现了不匹配的状况,在软件开发中称为接口不兼容,此时就需要适配器来进行一个接口转换。此时需要用一个 Adapter 层来进行接口转换。即:5V 电压就是 Target 接口,220V 电压就是 Adaptee 类,而将电压从 220V 转换到 5V 就是 Adapter。

  • 类适配器模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Target
public interface FiveVolt {
int getVolt5();
}
// Adaptee
public class Volt220 {
public int getVolt220(){
return 220;
}
}
// Adapter
public class VoltAdapter extends Volt220 implements FiveVolt {
@Override
public int getVolt5() {
return 5;
}
}
public class Test {
public static void main(String[] args){
VoltAdapter adapter = new VoltAdapter();
System.out.println("输出电压 : " + adapter.getVolt5());
}
}
  • 对象适配器模式
    Target 和 Adaptee 同上,但 Adapter 类不继承 Adaptee 类,而是代理,这比类适配器方式更为灵活。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class VoltAdapter implements FiveVolt {
private Volt220 mVolt220;
public VoltAdapter(Volt220 mVolt220) {
this.mVolt220 = mVolt220;
}
public int getVolt220(){
return mVolt220.getVolt220();
}
@Override
public int getVolt5() {
return 5;
}
}
public class Test {
public static void main(String[] args){
VoltAdapter adapter = new VoltAdapter(new Volt220());
System.out.println("输出电压 : "+adapter.getVolt5());
}
}

ANDROID 源码中的实现


在开发过程中,ListView 的 Adapter 是我们最常见的类型之一。ListView 中并没有 Adapter 相关的成员变量,其实 Adapter 在 ListView 的父类 AbsListView 中,AbsListView 是一个列表控件的抽象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
ViewTreeObserver.OnTouchModeChangeListener,
RemoteViewsAdapter.RemoteAdapterConnectionCallback {
...
ListAdapter mAdapter;
...
// 关联到 Window 时调用,获取调用 Adapter 中的 getCount 方法等
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
final ViewTreeObserver treeObserver = getViewTreeObserver();
treeObserver.addOnTouchModeChangeListener(this);
if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) {
treeObserver.addOnGlobalLayoutListener(this);
}
// 给适配器注册一个观察者
if (mAdapter != null && mDataSetObserver == null) {
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
// Data may have changed while we were detached. Refresh.
mDataChanged = true;
mOldItemCount = mItemCount;
// 获取 Item 的数量,调用的是 mAdapter 的 getCount 方法
mItemCount = mAdapter.getCount();
}
}
...
// 子类需要覆写 layoutChildren 函数来布局 child view,也就是 item view
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
// 布局 Child View
layoutChildren();
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
mInLayout = false;
}
...
}

ListView 实现了 layoutChildren 方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
protected void layoutChildren() {
...
try {
super.layoutChildren();
invalidate();
...
// 根据布局模式来布局 item view
switch (mLayoutMode) {
...
case LAYOUT_FORCE_TOP:
case LAYOUT_FORCE_BOTTOM:
case LAYOUT_SPECIFIC:
case LAYOUT_SYNC:
break;
case LAYOUT_MOVE_SELECTION:
default:
...
}
...
}
}

ListView 覆写了 AbsListView 中的 layoutChildren 函数,在该函数中根据布局模式来布局 item view,例如,默认情况是从上到下开始布局,但是,也有从下到上开始布局的,例如 QQ 聊天窗口的气泡布局,最新的消息就会布局到窗口的最底部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 从上往下填充 item view
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
// 通过 makeAndAddView 获取 item view
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
// 从下往上填充布局
private View fillUp(int pos, int nextBottom) {
View selectedView = null;
int end = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end = mListPadding.top;
}
while (nextBottom > end && pos >= 0) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
// 通过 makeAndAddView 获取 item view
View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
nextBottom = child.getTop() - mDividerHeight;
if (selected) {
selectedView = child;
}
pos--;
}
mFirstPosition = pos + 1;
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}

makeAndAddView 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// Try to use an existing view for this position.
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
// Found it. We're reusing an existing child, so it just needs
// to be positioned like a scrap view.
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// 获取一个 item view
// Make a new view for this position, or convert an unused view if
// possible.
final View child = obtainView(position, mIsScrap);
// 将 item view 设置到对应的地方
// This needs to be positioned and measured.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}

makeAndAddView 分为两个步骤,第一个是根据 position 获取一个 item view,然后将这个 view 布局到特定的位置。获取一个 item view 调用的是 obtainView 方法。这个方法在 AbsListView 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
View obtainView(int position, boolean[] outMetadata) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
outMetadata[0] = false;
// Check whether we have a transient state view. Attempt to re-bind the
// data and discard the view if we fail.
final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
// If the view type hasn't changed, attempt to re-bind the data.
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this);
// If we failed to re-bind the data, scrap the obtained view.
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
}
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
transientView.dispatchFinishTemporaryDetach();
return transientView;
}
// 1. 从缓存的 item view 中获取,ListView 的复用机制就在这里
final View scrapView = mRecycler.getScrapView(position);
// 2. 注意,这里将 scrapView 设置给了 Adapter 的 getView 函数
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
...
return child;
}

obtainView 方法定义了列表控件的 item view 的复用逻辑,首先会从 RecyclerBin 中获取一个缓存的 View。如果有缓存则将这个缓存的 View 传递到 Adapter 的 getView 的第二个参数中,这也就是我们对 Adapter 的最常见的优化方式,即判断 getView 的 convertView 是否为空。如果为空则从 xml 中创建视图,否则使用缓存的 View。这样避免了每次都从 xml 加载布局的消耗,能够显著提升 ListView 的效率。

在 ListView 的适配器模式中,target 角色就是 View,Adapter 就是将 item view 输出为 view 抽象的角色,adaptee 就是需要被处理的 item view。通过增加 adapter 一层来将 item view 的操作抽象起来,listView 等集合视图通过 adapter 对象获得 item 的个数、数据、item view 等,从而达到适配各种数据、各种 item 视图的效果。